﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;
using Platform.ObjectModel;
using Platform.ObjectModel.Design;
using System.Diagnostics;
using Platform.Storage.Entity;

namespace Platform.Storage.Impl
{
	/// <summary>
	/// Representa um contexto de tradução de uma expressão LINQ
	/// </summary>
	internal sealed class Context : IDisposable
	{
		public Context( ContextManager manager )
		{
			this.Manager = manager;
			//this.Criterias = new List<Expression<Func<FieldValue, bool>>>();
			this.Criterias = new List<TranslationCriteria>();
			this.LoadProperties = new List<string>();
		}

		/// <summary>
		/// Obtém a expressão com o IQueryable original do documento
		/// </summary>
		public Expression Entity
		{ get; set; }

		/// <summary>
		/// Obtém a expressão IQueryable com os campos que serão carregados para montar o documento
		/// </summary>
		public Expression QueryBase
		{ get; set; }

		/// <summary>
		/// Obtém a expressão com o número máximo de documentos que serão retornadas
		/// </summary>
		public Expression TakeCount
		{ get; set; }

		/// <summary>
		/// Obtém a expressão com o número de documentos que serão ignorados
		/// </summary>
		public Expression SkipCount
		{ get; set; }

		/// <summary>
		/// 
		/// </summary>
		public ContextManager Manager
		{ get; private set; }

		/// <summary>
		/// Obtém a lista de critérios para filtrar os documentos do repositório
		/// </summary>
		//public List<Expression<Func<FieldValue, bool>>> Criterias
		public List<TranslationCriteria> Criterias
		{ get; private set; }

		/// <summary>
		/// Obtém o nome que identifica o schema do documento
		/// </summary>
		public DataObjectName QualifiedName
		{ get; set; }

		/// <summary>
		/// Obtém o tipo do objeto que deve ser instânciado para carregar o documento
		/// </summary>
		public Type EntityType
		{ get; set; }

		/// <summary>
		/// 
		/// </summary>
		public IQueryResolver Resolver
		{ get; internal set; }

		/// <summary>
		/// 
		/// </summary>
		public Stack<string> Methods
		{ get; internal set; }

		/// <summary>
		/// 
		/// </summary>
		public string AggregateMethod
		{ get; private set; }

		/// <summary>
		/// OBtém ou define as propriedades que também devem ser carregadas por eagerLoad
		/// </summary>
		public List<string> LoadProperties
		{ get; private set; }

		/// <summary>
		/// Identifica se a expressão que está sendo traduzida é uma agregação
		/// </summary>
		public bool IsAggregate
		{ get { return !String.IsNullOrEmpty( this.AggregateMethod ); } }

		/// <summary>
		/// Sinaliza que um escopo foi finalizado
		/// </summary>
		/// <param name="method"></param>
		/// <returns></returns>
		public bool ScopeFinished( string method )
		{
			Trace.Assert( this.Methods.Peek() == method );
			this.Methods.Pop();
			return this.Methods.Count == 0;
		}

		/// <summary>
		/// Define que a query que está sendo tradução é uma agregação
		/// </summary>
		/// <param name="method"></param>
		public void SetAggregate( string method )
		{
			this.AggregateMethod = method;
		}

		/// <summary>
		/// Adiciona um critério de pesquisa do tipo '&&'
		/// </summary>
		/// <param name="node"></param>
		public void AddCriteria( BinaryExpression node )
		{
			this.AddCriteria( CriteriaType.Exclusion, node );
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="kind">Identifica o tipo de pesquisa</param>
		/// <param name="node"></param>
		public void AddCriteria( CriteriaType kind, BinaryExpression node )
		{
			// Procura pelo campo que será substituído
			MemberAccessFinder finder = new MemberAccessFinder( this.EntityType, this.Resolver );
			var member = finder.Find( node );

			// Valor utilizado para comparação
			var valueExp = node.Right.Equals( member.FieldExpression ) ? node.Left : node.Right;
			string propertyName = member.FieldExpression.GetPropertyName();
			string fieldName = member.GetFieldName( this.QualifiedName, propertyName ).ToLowerInvariant();

			// Se não é um relacionamento e o valor é uma constante, 
			// então formata o valor para o mesmo formato do campo
			//if ( !member.HasRelation
			//    && valueExp.Has<ConstantExpression>() )
			if ( valueExp.Has<ConstantExpression>() )
			{
				var field = this.Resolver.GetSchema( this.EntityType ).Fields[ propertyName ];

				if ( field != null )
				{
					// TODO : Temporáriamente está somente convertendo para string, no futuro tem que ser comparação por binários
					if ( field.Type == FieldType.Object )
					{
						valueExp = Expression.Constant(
							Encoding.Default.GetString( field.GetSerializer().ToStream( valueExp.CompileAndGetValue() ) ) );
					}
					else
					{
						valueExp = Expression.Constant( Convert.ToString( valueExp.CompileAndGetValue(), System.Globalization.CultureInfo.InvariantCulture ) );
					}

					// Realiza a serialização 
					//valueExp = Expression.Constant(
					//	field.GetSerializer().ToBase64String( valueExp.CompileAndGetValue() ) );
				}
			}
			

			// Cria o critério de busca
			Expression<Func<FieldValue, bool>> criteria =
				( field ) => field.LoweredFieldName == fieldName
						&& field.TextValue == "$$memberMarker_" ;

			// Troca "$$memberMarker_" pelo valor que deve ser filtrado e pela operação que deve ser aplicada
			criteria = criteria.Replace<BinaryExpression>(
				bin => bin.Right.Is<ConstantExpression>( c => c.Value.Equals( "$$memberMarker_" ) ),
				found => Expression.MakeBinary( found.NodeType, found.Left, valueExp ) ).CastToLambda();

			if ( member.HasRelation )
			{
				Expression<Func<DataObjectRelationship, bool>> relationFilter =
					( relation ) => this.Resolver.Fields.Where(
						childField => childField.LoweredFieldName.StartsWith( relation.ChildLoweredName )
										&& childField.ReversedTokenId == relation.ChildReversedTokenId )
									.Any( criteria );

				// Adiciona o critério que utiliza a relação, na lista 
				this.Criterias.Add( new TranslationCriteria( kind, fieldName, null, valueExp ) { RelationExpression = relationFilter } );
			}
			else
			{
				// Adiciona o critério na lista 
				this.Criterias.Add( new TranslationCriteria( kind, fieldName, criteria, valueExp ) );
			}
		}

		/// <summary>
		/// Cria o critério de filtro especificado pelo contexto
		/// </summary>
		/// <returns></returns>
		public IQueryable<string> CreateFilterCriteria()
		{
			IQueryable<string> tokenFilter = null;

			// Adiciona os critérios de pesquisa
			if ( this.Criterias.Count > 0 )
			{
				tokenFilter = this.Criterias[ 0 ].ToEntityFilter( this.Resolver );

				for ( int index = 1; index < this.Criterias.Count; index++ )
				{
					var criteria = this.Criterias[ index ];
					if ( criteria.KindOf == CriteriaType.Exclusion )
						tokenFilter = tokenFilter.Intersect( criteria.ToEntityFilter( this.Resolver ) );
					else
						tokenFilter = tokenFilter.Union( criteria.ToEntityFilter( this.Resolver ) );
				}
			}
			else
			{
				//string columnKey = this.Manager.Expander.GetFieldKey( this.EntityType );
				//tokenFilter = this.Manager.Expander.Fields.Where( field => field.LoweredFieldName == columnKey ).Select( f => f.ReversedTokenId );
				string columnKey = this.QualifiedName.GroupWith( this.Resolver.GetSchema( this.EntityType ).FieldKey ).ToLowerInvariant();
				tokenFilter = this.Resolver.Fields.Where( field => field.LoweredFieldName == columnKey ).Select( f => f.ReversedTokenId );
			}

			return tokenFilter;
		}

		/// <summary>
		/// Realiza a criação da query, baseado no contexto
		/// </summary>
		/// <returns></returns>
		public Expression CreateQuery()
		{
			//var context = this.contexts.Pop();
			var context = this;
			IQueryable<string> tokenFilter = context.CreateFilterCriteria();

			// Verifica se necessita ignora ou limitar a quantidade de
			// registros que serão retornados
			if ( context.SkipCount != null
				|| context.TakeCount != null )
			{
				if ( context.SkipCount != null )
					tokenFilter = tokenFilter.Skip( ( int )context.SkipCount.CompileAndGetValue() );

				if ( context.TakeCount != null )
					tokenFilter = tokenFilter.Take( ( int )context.TakeCount.CompileAndGetValue() );
			}

			// Realiza as agregações
			if ( context.IsAggregate )
			{
				return TranslatorHelper.MakeQueryableCall( typeof( string ), context.AggregateMethod, Expression.Constant( tokenFilter ) );
			}
			else
			{
				Expression<Func<FieldValue, bool>> fieldFilters = entityField => tokenFilter.Contains( entityField.ReversedTokenId );
				return TranslatorHelper.MakeQueryableCall( "Where", context.QueryBase, fieldFilters );
			}
		}

		#region IDisposable Members

		public void Dispose()
		{
			this.Manager.AllContexts.Pop();
		}

		#endregion

		#region [ class TranslationCriteria ]

		/// <summary>
		/// Conjunto de dados que representa um critério
		/// </summary>
		internal sealed class TranslationCriteria
		{
			public TranslationCriteria( CriteriaType kind, string fieldName, Expression<Func<FieldValue, bool>> expression, Expression value )
			{
				this.KindOf = kind;
				this.MainFieldName = fieldName;
				this.Expression = expression;
				this.ValueExpression = value;
			}

			public CriteriaType KindOf
			{ get; set; }

			public string MainFieldName
			{ get; set; }

			/// <summary>
			/// 
			/// </summary>
			public Expression<Func<FieldValue, bool>> Expression
			{ get; set; }

			/// <summary>
			/// 
			/// </summary>
			public Expression<Func<DataObjectRelationship, bool>> RelationExpression
			{ get; set; }

			/// <summary>
			/// Valor utilizado na expressão para fazer o filtro
			/// </summary>
			public Expression ValueExpression
			{ get; set; }

			/// <summary>
			/// 
			/// </summary>
			/// <param name="resolver"></param>
			/// <returns></returns>
			internal IQueryable<string> ToEntityFilter( IQueryResolver resolver )
			{
				if ( this.Expression == null )
					return resolver.Relations.Where( this.RelationExpression ).Select( r => r.ParentReversedTokenId );
				else
					return resolver.Fields.Where( this.Expression ).Select( f => f.ReversedTokenId );
			}
		}

		#endregion
	}
}
